Posted by Sam on Oct 18, 2006 at 11:12 AM UTC - 6 hrs
Well, I guess I lied when
I said xorBlog wouldn't be developed until I had caught up on my writing. I still haven't gotten caught up, but this morning I couldn't stand it any more - I had to have a way to categorize posts. Now, I didn't TDD these, and I didn't even put them in the right place. True to the name of the blog, I interspersed code where it was needed. I feel absolutely dirty, but I just couldn't spare the time at the moment to do it right, and I could no longer endure not having any categories. So, I took about 15 minutes, coded up a rudimentary category system, violated DRY in 2 places, and put a few comments like "this needs to be refactored into a CFC" throughout the code (as it needed).
At least I have some categories now (its not as gratifying a feeling as I thought it would be, however). I plan on refactoring this as soon as I have a chance. I'll write about it as well - it might make for some more interesting reading in the TDDing xorBlog series of posts.
Hey! Why don't you make your life easier and subscribe to the full post
or short blurb RSS feed? I'm so confident you'll love my smelly pasta plate
wisdom that I'm offering a no-strings-attached, lifetime money back guarantee!
Last modified on Oct 18, 2006 at 11:14 AM UTC - 6 hrs
Posted by Sam on Aug 29, 2006 at 09:41 AM UTC - 6 hrs
I had hoped to have more functionality in xorBlog by now, such as categories and comments, and a way for each post to have its own page.
However, as I've been writing the TDDing xorBlog series of posts, I've realized how much of a pain it is to write them ex post facto.
The pain comes from the fact that the code base is much more mature than what I'm writing about, so I'm having to edit a lot of the code for posting so that it matches my original thought process.
While it was good to get the blog up and running in the beginning, I'm going to catch up on the writing aspect before I add any more functionality. That way, I can write as I code. Doing that, the thoughts will be fresher and I won't have to edit the code.
Last modified on Aug 29, 2006 at 09:51 AM UTC - 6 hrs
Posted by Sam on Aug 29, 2006 at 09:27 AM UTC - 6 hrs
Now that we can insert posts, it is possible to update, select, delete, and search for them. To me, any
one of these would be a valid place to go next. However, since I want to keep the database as unchanged
as possible, I'll start with
test_deletePost()
. This way, as posts are inserted for
testing, we can easily delete them.
Here is the code I wrote in xorblog/cfcs/tests/test_PostEntity:
<cffunction name="test_deletePost"
access="public"
returnType="void"
output="false"
>
<cfset var local = structNew()>
<cfset local.newID=_thePostEntity.insertPost(name="blah"
, meat="blah"
, originalDate="1/1/1900"
, author="yoda"
)>
<cfset local.wasDeleted = _thePostEntity.deletePost(local.newID)>
<cfset assertTrue(condition=local.wasDeleted, message="The post was not deleted."
)>
<cfquery name="local.post"
datasource="#_datasource#"
>
select id from post where id = <cfqueryparam cfsqltype="cf_sql_integer"
value="#local.newID#"
>
</cfquery>
<cfset assertEquals(actual = local.post.recordcount, expected = 0)>
</cffunction>
And the corresponding code for
deletePost()
:
<cffunction name="deletePost"
output="false"
returntype="boolean"
access="public"
>
<cfargument name="id"
required="true"
type="numeric"
>
<cfset var local = structNew()>
<cfset local.result = false>
<cftry>
<cfquery name="local.del"
datasource="#_datasource#"
>
delete from post where id = <cfqueryparam cfsqltype="cf_sql_integer"
value="#id#"
>
</cfquery>
<cfset local.result=true>
<cfcatch>
</cfcatch>
</cftry>
<cfreturn local.result>
</cffunction>
Originally, I just left the test as asserting that
local.wasDeleted
was true. However, writing just
enough of
deletePost()
in xorblog/cfcs/tests/PostEntity to get the test to pass resulted in the
simple line
<cfreturn true>
. Since that would always pass, I also added a check that
the inserted post no longer existed.
Now that we have some duplicate code, its definitely time to do some refactoring. More on that next time.
(To be continued...)
Posted by Sam on Aug 28, 2006 at 11:46 AM UTC - 6 hrs
We left off after writing the test for the
insertPost()
method. Now, we're going to make
that test pass by writing the code for it. First you'll need to create PostEntity.cfc in the xorblog/cfcs/src
directory, and make sure to surround it in the proper
<cfcomponent>
tags.
What follows is that code:
<cffunction name="insertPost"
output="false"
returntype="numeric"
access="public"
>
<cfargument name="name"
required="true"
type="string"
>
<cfargument name="meat"
required="true"
type="string"
>
<cfargument name="originalDate"
required="true"
type="date"
>
<cfargument name="author"
required="true"
type="string"
>
<cfset var local = structNew()>
<cftransaction>
<cfquery name="local.ins"
datasource="#variables._datasource#"
>
insert into post
(name, meat, originalDate, lastModifiedDate, author)
values
(<cfqueryparam cfsqltype="cf_sql_varchar"
value="#arguments.name#"
>
,
<cfqueryparam cfsqltype="cf_sql_longvarchar"
value="#arguments.meat#"
>
,
<cfqueryparam cfsqltype="cf_sql_timestamp"
value="#arguments.originalDate#"
>
,
<cfqueryparam cfsqltype="cf_sql_timestamp"
value="#arguments.originalDate#"
>
,
<cfqueryparam cfsqltype="cf_sql_varchar"
value="#arguments.author#"
>
,
</cfquery>
<cfquery name="local.result"
datasource="#_datasource#"
>
select max(id) as newID from post
where originalDate=<cfqueryparam cfsqltype="cf_sql_timestamp"
value="#arguments.originalDate#"
>
and name=<cfqueryparam cfsqltype="cf_sql_varchar"
value="#arguments.name#"
>
</cfquery>
</cftransaction>
<cfif local.result.recordcount is 0
>
<cfthrow message="The new post was not properly inserted."
>
</cfif>
<cfreturn local.result.newID>
</cffunction>
There isn't really anything special here, unless you are new to Coldfusion. If that's the case, you'll
want to take note of the
<cfqueryparam>
tag - using it is considered a "best practice" by
most (if not all) experienced Coldfusion developers.
The other item of note is that if you were to run this code by itself,
it still wouldn't work, since we haven't defined
variables._datasource
. Many developers
would do this in a function called
init()
that they call each time they create an object. I've
done it as well.
I suppose if you were rigorously following the
YAGNI principle, you might wait until creating the next
method that would use that variable before defining it. I certainly like YAGNI, but my
OCD is not so bad that I won't occasionally allow
my
ESP to tell me that I'm going to use something,
even if I don't yet need it. With that said, I try only do it in the most obvious of cases, such as this one.
Now that we've written the code for
insertPost()
, its time to run the test again. Doing so,
I see that I have two test that run green (this one, and our
test_hookup()
from earlier.
We've gone red-green, so now it's time to refactor. Unfortunately, I don't see any places to do that
yet, but I think they'll reveal themselves next time when we write
our second test and second method in
PostEntity
. (To be continued...)
Last modified on Aug 28, 2006 at 11:52 AM UTC - 6 hrs
Posted by Sam on Aug 25, 2006 at 12:34 PM UTC - 6 hrs
So we decided that blog software centers around posts and that for any other feature
to be useful, we'd need them first. Therefore, we'll start with a model component for our posts,
and we'll call it
PostEntity
. Before I create that file though, I'm going to go back
into my test_PostEntity.cfc file and write a test or two for some functionality that
PostEntity
should provide.
Thinking of things we should be able to do regarding the storage of posts, it's easy to identify
at least
insert()
,
update()
, and
delete()
. However,
since you can't update or delete a post that doesn't exist, I figured I'd start with adding a post.
I came up with the following test:
<cffunction name="test_insertPost"
access="public"
returntype="void"
output="false"
>
<cfset var local = structNew()>
<cfset local.nameOfPost = "My Test Post"
& left(createUUID(),8
)>
<cfset local.meatOfPost = "The meat of the post is that this is a test."
& left(createUUID(),8
)>
<cfset local.dateOfPost = now()>
<cfset local.author = "Sam #createUUID()#"
>
<cfset local.newID=_thePostEntity.insertPost(name=local.nameOfPost, meat=local.meatOfPost, originalDate=local.dateOfPost, lastModifiedDate=local.dateOfPost, author=local.author)>
<cfquery name="local.post"
datasource="#variables._datasource#"
>
select name, meat, originalDate, author
from post
where id = <cfqueryparam cfsqltype="cf_sql_integer"
value="#local.newID#"
>
</cfquery>
<cfset assertEquals(actual=local.post.name, expected=local.nameOfPost)>
<cfset assertEquals(actual=local.post.meat, expected=local.meatOfPost)>
<cfset assertEquals(actual=local.post.author, expected=local.author)>
<!--- dateCompare isn't working correctly, so we are testing each datepart --->
<cfset assertEquals(actual=month(local.post.originalDate), expected=month(local.dateOfPost))>
<cfset assertEquals(actual=day(local.post.originalDate), expected=day(local.dateOfPost))>
<cfset assertEquals(actual=year(local.post.originalDate), expected=year(local.dateOfPost))>
<cfset assertEquals(actual=hour(local.post.originalDate), expected=hour(local.dateOfPost))>
<cfset assertEquals(actual=minute(local.post.originalDate), expected=minute(local.dateOfPost))>
<cfset assertEquals(actual=second(local.post.originalDate), expected=second(local.dateOfPost))>
<!--- clean up --->
<cfquery datasource="#_datasource#"
>
delete from post where id = #local.newID#
</cfquery>
</cffunction>
You'll notice I used a UUID as part of the data. There's no real point to it, I suppose. I just wanted to have
different data each time, and thought this would be a good way to achieve that.
You should also be uncomfortable about the comment saying dateCompare isn't working - I am anyway. It doesn't
always fail, but occasionally it does, and for reasons I can't figure out, CFUnit isn't reporting why. For
now, so I can move on, I'm assuming it is a bug in CFUnit. Since I can test each date part that is
important to me individually and be sure the dates are the same if they all match, I don't feel
too bad.
Another thing to note is the use of the
var local
. By default, any variables created
are available everywhere, so to keep them local to a function, you need to use the
var
keyword.
I like to just create a
struct
called
local
and put all the local variables
in there - it just makes things easier.
Finally, some people might not like the length of that test. Right now, I don't either, but we'll see
what we can do about that later. Others may also object to using more than one assertion per test. I don't
mind it so much in this case since we really are only testing one thing. If you like, you could also
create a struct out of each and write a
UDF like
structCompare()
and do the assertion that way. I haven't tested this one personally, but
there is one available at
cflib.
In either case, I don't see much difference, other than one way I have to write more code than I need.
Now I run the test file we created and find that, as expected, the test still fails. Besides the fact that
we don't even have a PostEntity.cfc, we haven't yet instantiated an object of that type, nor have
we defined
_datasource
and the like. Let's do that in the
setUp()
method.
<cffunction name="setUp"
access="public"
returntype="void"
output="false"
>
<cfset variables._datasource="xorblog"
>
<cfset variables.pathToXorblog = "domains.xorblog"
>
<cfset variables._thePostEntity = createObject("component"
, "#variables.pathToXorblog#cfcs.src.PostEntity"
).init(datasource=_datasource)>
</cffunction>
Now our tests still fail, because we have no code or database. So create the datasource and
database with columns as needed:
id (int, primary key, autonumber)
name (nvarchar 50)
meat (ntext)
originalDate (datetime)
lastModifiedDate (datetime)
author (nvarchar 50)
Next time, we'll start coding and get our first green test. (To be continued...)
Last modified on Aug 29, 2006 at 08:38 AM UTC - 6 hrs
Posted by Sam on Aug 20, 2006 at 10:38 AM UTC - 6 hrs
Since I wanted to start this blog, I thought it would be good practice to write the software that runs it
using test-driven development. I've used a bit of TDD recently for additions to existing applications, but
I've not yet
started writing an application using it from beginning to end. I'm getting sick of
eating Italian microwaveable dinners when I have to maintain code. This is my chance to eat something else.
So, without further ado, we'll jump right in.
The first thing I did of course, was to create my directory structure. For the time being, we have:
xorblog/cfcs/src
and
xorblog/cfcs/tests
I like to keep the tests separate from the source. I don't have a reason behind it, other than it helps
keep me a bit organized.
Next, I thought about what a blog needs. We want to deliver items that have the highest business value first,
and move on to things that are lower on the value scale later. In doing this, we get a working application
sooner rather than later, and hence the blog can be used at the earliest possible moment in its development.
With that in mind, we probably shouldn't start with things like
Comments
or functionality
that lets us get included in places like
Technorati. Since
you need content to make anything else useful, I thought I'd start with that. Indeed, the
Post
is
the core part of a blog. Therefore, the first thing I did was create test_PostEntity.cfc under
xorblog/cfcs/tests.
Now, I'm using CFUnit for my tests, and this assumes you already have it set up. If you need help on
that, you can visit
CFUnit on SourceForge.
The first thing I do in test_PostEntity.cfc is write
test_hookup()
,
to make sure everything is working:
<cfcomponent extends="net.sourceforge.cfunit.framework.TestCase"
output="false"
name="test_PostEntity"
>
<cffunction name="test_hookup"
access="public"
returntype="void"
output="false"
>
<cfset assertEquals(expected=4, actual=2+2)>
</cffunction>
</cfcomponent>
Next, we need a way to see the status of and run our tests. For this we have test_runner.cfm, which
for the most part just copies what you'll find at the CFUnit site linked above:
<cfset testClasses = ArrayNew(1
)>
<cfset ArrayAppend(testClasses, "domains.xorblog.cfcs.tests.test_PostEntity"
)>
<!--- Add as many test classes as you would like to the array --->
<cfset suite = CreateObject("component"
, "globalcomponents.net.sourceforge.cfunit.framework.TestSuite"
).init( testClasses )>
<cfoutput>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
>
<html>
<head>
<title>
Unit Tests for xorBlog</title>
</head>
<body>
<h1>
xorBlog Unit Tests</h1>
<cfscript>
createobject("component"
, "globalcomponents.net.sourceforge.cfunit.framework.TestRunner"
).run(suite,'');
</cfscript>
</body>
</html>
</cfoutput>
Finally, we run that page in a browser to make sure the test runs green - and it does.
Now that we have our test environment set up, we can start writing tests for our
PostEntity
that doesn't yet exist. (To be continued...)
Last modified on Aug 20, 2006 at 10:49 AM UTC - 6 hrs